package macrotools;
import macrotools.error.Errors;
import haxe.macro.Expr;

/**
 * A set of common tools for manipulating macro data
 * @author waneck
 */

class MacroTools 
{
	public static function stringToComplexType(str:String):ComplexType
	{
		var params = [];
		if (str.indexOf("<") != -1)
		{
			var index = str.indexOf("<");
			var p = str.substr(index);
			str = str.substr(0,index);

			p = p.substr(0,p.length-1);
			for (p in p.split(","))
				params.push(TPType(stringToComplexType(p)));
		}
		var pack = str.split(".");
		var name = pack.pop();
		return TPath({pack:pack, name:name, params:params, sub:null});
	}
	
	public static function ensureType<T>(val:Dynamic, cls:Class<T>, pos:Position):T
	{
		if (!Std.is(val, cls))
		{
			throw new Error("Expected argument of type " + Type.getClassName(cls) + " but got " + Type.getClassName(Type.getClass(val)), pos );
		}
		
		return cast val;
	}
	
	public static function getTypePath(type:ComplexType):Null<String>
	{
		return switch(type)
		{
			case TPath(p): var pack = p.pack.copy(); pack.push(p.name); pack.join(".");
			default: null;
		}
	}
	
	/**
	 *  A common utility to work with constant arguments, like to get values out of metadata parameters
	 **/
	public static function parseArguments(args:Array<Expr>, acceptNamed:Bool = true, acceptIdent = false, strict = true, minArgs:Null<Int> = null, maxArgs:Null<Int> = null, neededNamed:Array<String> = null, ?pos:Position):Null<{args:Array<Dynamic>, namedArgs:Hash<Array<Dynamic>>}>
	{
		var retArgs = [];
		var retNamed = new Hash();
		
		for (arg in args)
		{
			var const = constant(arg, acceptIdent, false);
			if (const == null)
			{
				if (!acceptNamed) if (strict) throw new Error("Unexpected non-constant expression", arg.pos); else return null;
				switch(arg.expr)
				{
					case EBinop(op, e1, e2):
						switch(op)
						{
							case OpAssign:	
								var name = getString(e1, true, strict);
								if (name == null) return null;
								var arr = retNamed.get(name);
								if (arr == null)
								{
									arr = [];
									retNamed.set(name, arr);
								}
								
								var rightSide = constant(e2, acceptIdent, strict);
								if (rightSide == null) return null;
								arr.push(rightSide);
							default:
								if (strict) throw new Error("Unexpected non-constant expression", arg.pos); else return null;
						}
					default: if (strict) throw new Error("Unexpected non-constant expression", arg.pos); else return null;
				}
			} else {
				retArgs.push(const);
			}
			
			
		}
		
		if (minArgs != null && retArgs.length < minArgs )
		{
			throw new Error("Too few arguments.", pos);
		}
		
		if (maxArgs != null && retArgs.length > maxArgs)
		{
			throw new Error("Too many arguments.", pos);
		}
		
		if (neededNamed != null)
		{
			for (name in neededNamed)
			{
				if (!retNamed.exists(name))
				{
					throw new Error("Argument '" + name + "' is not optional but is not present", pos);
				}
			}
		}
		
		return {args:retArgs, namedArgs:retNamed};
	}
	
	public static function constant(e:Expr, acceptIdent = false, strict = true):Null<Dynamic>
	{
		switch(e.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CInt( v ): return  Std.parseInt(v);	
					case CFloat( f ):  return Std.parseFloat(f);
					case CString( s ):  return s;
					case CIdent( s ), CType( s ): 
						return if (!acceptIdent)
							if (strict) throw new Error("Invalid non-constant value.", e.pos); else null;
						else
							s;
					case CRegexp( r, opt ): return new EReg(r, opt);
				}
			case EArrayDecl(el):
				var retarr = [];
				for (e in el) retarr.push(constant(e, acceptIdent, strict));
				return retarr;
			case EBinop(op, e1, e2):
				var e1 = constant(e1, acceptIdent, strict);
				var e2 = constant(e2, acceptIdent, strict);
				if (e1 == null || e2 == null)
					if (strict) throw new Error("Invalid non-constant value.", e.pos); else return null;
				
				switch(op)
				{
					case OpAdd: return e1 + e2;
					case OpMult: return e1 * e2;
					case OpDiv: return e1 / e2;
					case OpSub: return e1 - e2;
					case OpAnd: return e1 & e2;
					case OpOr: return e1 | e2;
					case OpXor: return e1 ^ e2;
					case OpShl: return e1 << e2;
					case OpShr: return e1 >> e2;
					case OpUShr: return e1 >>> e2;
					case OpMod: return e1 % e2;
					default:
						if (strict) throw new Error("Invalid operation.", e.pos); else return null;
				}
			case EType(_, _), EField(_,_):
				return if (acceptIdent) 
					getPath(e, strict).join(".");
				else 
					if (strict) throw new Error("Invalid non-constant value.", e.pos); else null;
			case EParenthesis(e):
				return constant(e, acceptIdent, strict);
			default:
				return if (strict) throw new Error("A constant expression is required.", e.pos); else return null;
		}
	}
	
	/**
	 *  Gets a string from a constant; Either a real string type constant, or an ident/type if acceptIdent = true
	 *  @throws UnexpectedType when throwExceptions is set
	 **/
	public static function getString(e:Expr, ?acceptIdent = false, ?throwExceptions = true):Null<String>
	{
		var ret = switch(e.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CString(s): s;
					case CIdent(s), CType(s): if (acceptIdent) s; else throw UnexpectedType(["CString"], e.expr, e.pos);
					default: null;
				}
			default: null;
		}
		if (throwExceptions && ret == null)
			throw UnexpectedType((acceptIdent) ? ["CString", "CIdent", "CType"] : ["CString"], e.expr, e.pos);
		return ret;
	}
	
	/**
	 *  Gets an int from a constant Expr
	 * 	@throws UnexpectedType when throwExceptions is set
	 **/
	public static function getInt(e:Expr, ?throwExceptions = true):Null<Int>
	{
		var ret = switch(e.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CInt(s): Std.parseInt(s);
					default: null;
				}
			default: null;
		}
		
		if (throwExceptions && ret == null)
			throw UnexpectedType(["CInt"], e.expr, e.pos);
		return ret;
	}
	
	/**
	 *  Gets a float from a constant Expr
	 * 	@throws UnexpectedType when throwExceptions is set
	 **/
	public static function getFloat(e:Expr, ?acceptInt = true, ?throwExceptions = true):Null<Float>
	{
		var ret = switch(e.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CInt(s): if(acceptInt) Std.parseFloat(s); else null;
					case CFloat(s): Std.parseFloat(s);
					default: null;
				}
			default: null;
		}
		
		if (throwExceptions && ret == null)
			throw UnexpectedType((acceptInt) ? ["CFloat", "CInt"] : ["CFloat"], e.expr, e.pos);
		return ret;
	}
	
	/**
	 *  Returns if the underlying expression is an EDisplay or EDisplayNew. 
	 *  This is useful so we can identify 
	 **/
	public static function isDisplay(e:Expr):Bool
	{
		return switch(e.expr)
		{
			case EDisplay(_,_), EDisplayNew(_): true;
			default: false;
		}
	}
	
	/**
	 *  Gets a path from a series of EType/EField and EConst
	 **/
	public static function getPath(e:Expr, ?a:Array<String>, ?throwExceptions = true):Array<String>
	{
		if (null == a)
		{
			a = [];
		}
		
		var ret = switch(e.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CIdent(s), CType(s): a.push(s); a;
					default: null;
				}
			case EType(e, f), EField(e, f):
				getPath(e, a, throwExceptions);
				a.push(f);
				a;
			default: null;
		}
		
		if (throwExceptions && ret == null)
			throw "";// new Error("Unexpected Type", e.pos);
		return ret;
	}
	
	public static function mkCall(_path:Array<String>, arguments:Array<Expr>, pos:Position):Expr
	{
		return {expr:ECall(path(_path, pos), arguments), pos:pos};
	}
	
	
	public static function separatePackage(a:Array<String>) : { pack:Array<String>, type:String, fields:Array<String> }
	{
		var pack = [];
		var i = 0;
		var len = a.length;
		while(i < len)
		{
			var val = a[i];
			var fst = val.charCodeAt(0);
			if (fst >= 'A'.code && fst <= 'Z'.code) //is upper case
				break;
			
			pack.push(val);
			i++;
		}
		
		var type = a[i++];
		var fields = [];
		for (j in i...len)
			fields.push(a[j]);
		
		return { pack:pack, type:type, fields:fields };
	}
	
	public static function mk(e:ExprDef, p:Position):Expr
	{
		return { expr:e, pos:p };
	}
	
	public static function mkc(e:ExprDef, expr:Expr):Expr
	{
		return { expr:e, pos:expr.pos };
	}
	
	public static function isUpperFirst(string:String) : Bool
	{
		var fst = string.charCodeAt(0);
		return fst >= 'A'.code && fst <= 'Z'.code;
	}
	
	public static function ident(field:String, pos:Position):Expr
	{
		return (isUpperFirst(field)) ? mk(EConst(CType(field)), pos) : mk(EConst(CIdent(field)), pos);
	}
	
	public static function path(p:Array<String>, pos) : Expr
	{
		var len = p.length;
		switch(len)
		{
			case 0: throw "Inexistent path";
			case 1: 
				var field = p.pop();
				return (isUpperFirst(field)) ? mk(EConst(CType(field)), pos) : mk(EConst(CIdent(field)), pos);
			default:
				var field = p.pop();
				return (isUpperFirst(field)) ? mk(EType( path(p, pos), field), pos) : mk(EField(path(p,pos), field), pos);
		}
			
	}
	
}